#ifndef __M_THREAD_POOL_H__
#define __M_THREAD_POOL_H__

#include "noncopy.h"
#include <thread>
#include <vector>
#include <queue>
#include <condition_variable>
#include <mutex>
#include <functional>
#include <memory>
#include <future>

//
// https://github.com/progschj/ThreadPool
// https://github.com/Camio1945/ThreadPool
//
class ThreadPool : public NonCopyable {
private:
	// need to keep track of threads so we can join them
	std::vector<std::thread> workers_;
	// the task queue
	std::queue< std::function<void()> > tasks_;

	// synchronization
	bool stop_ = false;
	std::condition_variable condition_;
	std::mutex queue_mutex_;

public:
	ThreadPool(int threads) {
		for (int i = 0; i < threads; i++) {
			workers_.emplace_back([this] {
				while (true) {
					std::function<void()> task;

					{
						std::unique_lock<std::mutex> lock(this->queue_mutex_);
						this->condition_.wait(lock, [this] {
							return this->stop_ || !this->tasks_.empty();
						});

						if (this->stop_ && this->tasks_.empty()) {
							return;
						}

						task = std::move(this->tasks_.front());
						this->tasks_.pop();
					}

					task();
				}
			});
		}
	}

	~ThreadPool() {
		{
			std::unique_lock<std::mutex> lock(queue_mutex_);
			stop_ = true;
		}
		condition_.notify_all();

		for (auto& worker : workers_) {
			worker.join();
		}
	}

	// add new work item to the pool
	template<class F, class... Args>
	auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {
		using return_type = typename std::result_of<F(Args...)>::type;

		auto task = std::make_shared< std::packaged_task<return_type()> >(
			std::bind(std::forward<F>(f), std::forward<Args>(args)...));

		auto res = task->get_future();
		{
			std::unique_lock<std::mutex> lock(queue_mutex_);
			if (stop_) {
				throw std::runtime_error("enqueue on stopped ThreadPool");
			}

			tasks_.emplace([task]() { (*task)(); });
		}
		condition_.notify_one();
		return res;
	}
};

#endif // __M_THREAD_POOL_H__
